"""
Resize Pictures by Timothy A Wiseman
Version: 0.1
Version Date 30 OCT 2009

All heavy lifting is done by PIL from Pythonware with this 
script being little more than an interface for some 
of its commands.

The interface was written in wxPython.
"""

#################################################################
# Imports
#################################################################
import os.path
from PIL import Image
#command line paramaters, only needed if this is main
from optparse import OptionParser 

import wx

#################################################################
# Constants
#################################################################
rs = Image.ANTIALIAS #Resample type, aliased for conveniance 

helpStmt = """This program will resize all pictures in the 
input directory and place the modified version in the output
directory.  If a prefix or suffix is supplied it will be
added to all file names in the output directory.  

The modification can be made on an absolute basis, 
where all output files will be of exactly the size specified.
It can also be made on a maximum basis, where all files will
come as close to, but not over, the specified size as possible
while still maintaining the aspect ratio.  In other words,
an absolute adjustment may distort the image, but a 
maximum adjustment will not.  It can also be changed to a 
percentage of its current size.

If the output and input directories are the same
and no prefix or suffix are specified, all image
files will be overwritten."""

aboutStmt = """
Author: Timothy A Wiseman
Version .1 on 09 NOV 09.
Written in Python using the PIL image library
and wxPython gui library.  

Spyder was used as the IDE."""


##################################################################
#Functions
##################################################################

def absResize(img, sizeTuple):
    """Simple call to Img.Resize.  Straight passthrough.
    Exists in order to more easily incorporate into a dictionary
    of functions that expects to take an image as a paramater."""

    return img.resize(sizeTuple, rs)

def maxResize(img, maxSizeTuple):
    """Takes an image and a maximum size tuple.
    It then adjusts the images size to the maximum size that is less than
    or equal to the sizeTuple and maintains the previous aspect ratio."""
    minRatio = min(
                    float(maxSizeTuple[0])/float(img.size[0]),
                    float(maxSizeTuple[1])/float(img.size[1])
                    )
    #int will automatically take the floor of the value
    sizeTuple = (
                    int(img.size[0] * minRatio),
                    int(img.size[1] * minRatio)
                )
    return img.resize(sizeTuple, rs)

def pctResize(img, pct):
    """Takes an image and a percentage and returns a reference to an
    image that has been scaled by that percentage."""
    pct = pct/100.0
    #int will automatically take the floor of the value
    sizeTuple = (
                    int(img.size[0] * pct),
                    int(img.size[1] * pct)
                )
    return img.resize(sizeTuple, rs)

def adjName(path, name, prefix='', suffix = ''):
    """Takes a file name, inserts the suffix before the extension and/or
    the prefix at the beginning, then adds the path and returns.
    """
    if len(prefix) != 0:
        name = prefix + name
        
    if len(suffix) != 0:
        period = name.rindex('.')
        name = name[0:period] + suffix + name[period:]

    return os.path.join(path, name)
    
def dupFiles(inFolder, outFolder='', prefix='', suffix=''):
    """Returns a list of file names that would be overwritten
    if the process is completed."""
    dupFiles = []
    if len(outFolder) == 0 :
        outFolder = inFolder
    for outFile in [adjName(outFolder, x, prefix, suffix)
                    for x in os.listdir(inFolder) if
                    os.path.isfile(os.path.join(inFolder,x))]:
        if os.path.isfile(outFile):
            dupFiles.append(outFile)
    return dupFiles

def processFolder(inFolder, adj, adjType, outFolder='', prefix='', suffix = ''):
    """Processes the passed in folder.  It requires the folder to process, the
    value to adjust too, and the adjustment type.  The expected value type for
    ajd depends on the adjustment type.  For max or abs it is looking for a two
    tuple of numbers.  For pct it is looking for a single number.
    It returns a list of the file names that could not be processed.

    Optionally, it will take an output folder, a pre/suf-fix to add and a 0 
    indicating a prefix or 1 indicating a suffix.  If the outFolder is 
    the same as the inFolder and no pre/suf-fix is supplied, it 
    will overwrite all image files in that folder.
    
    If the output folder does not exist, it will be created."""
    couldNotProcess = []
    if len(outFolder) == 0 :
        outFolder = inFolder
    if os.path.isdir(outFolder) == False:
        os.mkdir(outFolder)
    for file in [x for x in os.listdir(inFolder) if
                 os.path.isfile(os.path.join(inFolder,x))]:
        try:
            img = Image.open(os.path.join(inFolder,file))
            img = {'abs': absResize,
                   'max': maxResize,
                   'pct': pctResize}[adjType](img, adj)
            newFileName = adjName(outFolder, file, prefix, suffix)
            img.save(newFileName)
        except Exception, inst:
            couldNotProcess.append(file)
            #send the reason to console for troubleshooting, end user 
            #should not see if run under a gui.
            print 'Failed on : ' + file
            print inst
            print '\n'
    return couldNotProcess
    
##################################################################
#Gui Class
##################################################################

class GuiFrame(wx.Frame):
    def __init__(self, *args, **kwds):
        # begin wxGlade: GuiFrame.__init__
        kwds["style"] = wx.DEFAULT_FRAME_STYLE
        wx.Frame.__init__(self, *args, **kwds)
        
        # Menu Bar
        self.GuiMenubar = wx.MenuBar()
        self.File = wx.Menu()
        self.Exit = wx.MenuItem(self.File, wx.NewId(), "Exit", "Terminate Program", wx.ITEM_NORMAL)
        self.File.AppendItem(self.Exit)
        self.GuiMenubar.Append(self.File, "File")
        self.HelpMenu = wx.Menu()
        self.Instructions = wx.MenuItem(self.HelpMenu, wx.NewId(), "Instructions", "Instructions for use", wx.ITEM_NORMAL)
        self.HelpMenu.AppendItem(self.Instructions)
        self.About = wx.MenuItem(self.HelpMenu, wx.NewId(), "About", "About the Program", wx.ITEM_NORMAL)
        self.HelpMenu.AppendItem(self.About)
        self.GuiMenubar.Append(self.HelpMenu, "Help")
        self.SetMenuBar(self.GuiMenubar)
        # Menu Bar end
        self.GuiStatusbar = self.CreateStatusBar(1, 0)
        self.radio_box_1 = wx.RadioBox(self, -1, "Adjustment Type", 
                                       choices=["Maximum", "Absolute", "Percentage"], 
                                       majorDimension=1, style=wx.RA_SPECIFY_ROWS)
        self.WidthLbl = wx.StaticText(self, -1, "Width")
        self.WidthTxt = wx.TextCtrl(self, -1, "640")
        self.HeightLbl = wx.StaticText(self, -1, "Height:")
        self.HeightTxt = wx.TextCtrl(self, -1, "480")
        self.PctLbl = wx.StaticText(self, -1, "Percent:")
        self.PctTxt = wx.TextCtrl(self, -1, "50")
        self.PrefixLbl = wx.StaticText(self, -1, "Prefix (Optional):")
        self.PrefixTxt = wx.TextCtrl(self, -1, "")
        self.SuffixLbl = wx.StaticText(self, -1, "Suffix (Optional):")
        self.SuffixTxt = wx.TextCtrl(self, -1, "")
        self.SrcBtn = wx.Button(self, -1, "Source Dir:")
        self.SrcTxt = wx.TextCtrl(self, -1, "")
        self.DestBtn = wx.Button(self, -1, "Dest Dir:")
        self.DestTxt = wx.TextCtrl(self, -1, "")
        self.button_1 = wx.Button(self, wx.ID_OK, "")
        self.button_2 = wx.Button(self, wx.ID_CANCEL, "")

        self.__set_properties()
        self.__do_layout()
        # end wxGlade
        
        #Control Binding
        wx.EVT_RADIOBOX(self, self.radio_box_1.GetId(), self.adjRadioBox)
        
        wx.EVT_BUTTON(self, self.SrcBtn.GetId(), self.SelectSrcDir)
        wx.EVT_BUTTON(self, self.DestBtn.GetId(), self.SelectDestDir)
        wx.EVT_BUTTON(self, self.button_2.GetId(), self.close)
        wx.EVT_BUTTON(self, self.button_1.GetId(), self.processBtn)
        
        wx.EVT_MENU(self, self.Exit.GetId(), self.close)
        wx.EVT_MENU(self, self.Instructions.GetId(), self.instructionsMnu)
        wx.EVT_MENU(self, self.About.GetId(), self.aboutMnu)
        #self.radio_box_1.bind(wx.EVT_RADIOBOX, self.adjRadioBox, 
        #                      id = self.radio_box_1.id)

    def __set_properties(self):
        # begin wxGlade: GuiFrame.__set_properties
        self.SetTitle("Resize Pictures")
        self.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
        self.GuiStatusbar.SetStatusWidths([-1])
        # statusbar fields
        GuiStatusbar_fields = ["Image Resizer"]
        for i in range(len(GuiStatusbar_fields)):
            self.GuiStatusbar.SetStatusText(GuiStatusbar_fields[i], i)
        self.radio_box_1.SetSelection(0)
        self.PctLbl.Disable()
        self.PctTxt.Disable()
        self.SrcTxt.SetMinSize((350,-1))
        # end wxGlade

    def __do_layout(self):
        # begin wxGlade: GuiFrame.__do_layout
        sizer_1 = wx.BoxSizer(wx.VERTICAL)
        sizer_3 = wx.BoxSizer(wx.HORIZONTAL)
        sizer_2 = wx.BoxSizer(wx.VERTICAL)
        grid_sizer_1 = wx.FlexGridSizer(7, 2, 0, 0)
        sizer_2.Add(self.radio_box_1, 0, wx.EXPAND|wx.ALIGN_CENTER_HORIZONTAL|wx.ALIGN_CENTER_VERTICAL, 0)
        grid_sizer_1.Add(self.WidthLbl, 0, 0, 0)
        grid_sizer_1.Add(self.WidthTxt, 0, 0, 0)
        grid_sizer_1.Add(self.HeightLbl, 0, 0, 0)
        grid_sizer_1.Add(self.HeightTxt, 0, 0, 0)
        grid_sizer_1.Add(self.PctLbl, 0, 0, 0)
        grid_sizer_1.Add(self.PctTxt, 0, 0, 0)
        grid_sizer_1.Add(self.PrefixLbl, 0, 0, 0)
        grid_sizer_1.Add(self.PrefixTxt, 0, 0, 0)
        grid_sizer_1.Add(self.SuffixLbl, 0, 0, 0)
        grid_sizer_1.Add(self.SuffixTxt, 0, 0, 0)
        grid_sizer_1.Add(self.SrcBtn, 0, 0, 0)
        grid_sizer_1.Add(self.SrcTxt, 0, wx.EXPAND, 0)
        grid_sizer_1.Add(self.DestBtn, 0, 0, 0)
        grid_sizer_1.Add(self.DestTxt, 1, wx.EXPAND, 0)
        sizer_2.Add(grid_sizer_1, 1, wx.ALL|wx.EXPAND, 5)
        sizer_1.Add(sizer_2, 1, wx.EXPAND, 0)
        sizer_3.Add((250, 20), 0, 0, 0)
        sizer_3.Add(self.button_1, 0, wx.ALL, 3)
        sizer_3.Add(self.button_2, 0, wx.ALL, 3)
        sizer_1.Add(sizer_3, 0, wx.EXPAND, 0)
        self.SetSizer(sizer_1)
        sizer_1.Fit(self)
        self.Layout()
        # end wxGlade
        
    def adjRadioBox(self, event):
        """Hide or reveal options depending on the radio box button"""
        radioBox = event.GetEventObject()
        radioString = radioBox.GetStringSelection()
        if radioString == "Percentage":
            self.PctLbl.Enable()
            self.PctTxt.Enable()
            self.WidthLbl.Disable()
            self.WidthTxt.Disable()
            self.HeightLbl.Disable()
            self.HeightTxt.Disable()
        else:
            self.PctLbl.Disable()
            self.PctTxt.Disable()
            self.WidthLbl.Enable()
            self.WidthTxt.Enable()
            self.HeightLbl.Enable()
            self.HeightTxt.Enable()
            
    def SelectSrcDir(self, event):
        DirName = wx.DirSelector()
        self.SrcTxt.SetValue(DirName + "\\")
        if len(self.DestTxt.GetValue()) == 0:
            self.DestTxt.SetValue(DirName + '\\Resized\\')
        
    def SelectDestDir(self, event):
        DirName = wx.DirSelector()
        self.DestTxt.SetValue(DirName + "\\")
        
    def close(self, event):
        self.Destroy()
            
    def processBtn(self, event):
        #Check for files to be overwritten and warn user.
        confirm = wx.ID_YES #assume it can proceed
        dups = dupFiles(inFolder=self.SrcTxt.GetValue(),
                    outFolder=self.DestTxt.GetValue(),
                    prefix=self.PrefixTxt.GetValue(), 
                    suffix = self.SuffixTxt.GetValue())
        if len(dups) > 0:
            msg = ('The following files will be overwritten: ' + 
                    ', \n'.join(dups) + '\n\n'
                    'Proceed?')
            md = wx.MessageDialog(self, msg, 'Confirm Overwrite', wx.YES_NO)
            confirm = md.ShowModal()
            print confirm
            md.Destroy()
        if confirm == wx.ID_YES:
            radioString = self.radio_box_1.GetStringSelection()
            if radioString == "Percentage":
                adj = int(self.PctTxt.GetValue())
                adjType = 'pct'
            elif radioString == "Absolute":
                adj = (
                        int(self.WidthTxt.GetValue()), 
                        int(self.HeightTxt.GetValue())
                        )
                adjType = 'abs'
            elif radioString == "Maximum":
                adj = (
                        int(self.WidthTxt.GetValue()), 
                        int(self.HeightTxt.GetValue())
                        )
                adjType = 'max'
            couldNotProcess = processFolder( 
                                inFolder = self.SrcTxt.GetValue(),
                                adj= adj, 
                                adjType=adjType, 
                                outFolder=self.DestTxt.GetValue(),
                                prefix=self.PrefixTxt.GetValue(), 
                                suffix = self.SuffixTxt.GetValue())
            if len(couldNotProcess) == 0:
                msg = "All files processed.  Process another folder?"
                md = wx.MessageDialog(self, msg, 'Success', wx.YES_NO)
                another = md.ShowModal()
                print another
                md.Destroy()
            else:
                msg = ("Could not process: \n" + 
                        ', \n'.join(couldNotProcess) + '\n' +
                        ' Process another folder?')
                md = wx.MessageDialog(self, msg, 'Success', wx.YES_NO)
                another = md.ShowModal()
                print another
                md.Destroy()
            if another != wx.ID_YES:
                self.Destroy()
 
    def instructionsMnu(self, event):
        wx.MessageBox(helpStmt, 'Instructions')
        
    def aboutMnu(self, event):
        wx.MessageBox(aboutStmt, 'About')

# end of class GuiFrame

##################################################################
#Main loop.
##################################################################

#Check for CLI for a folder ot process, if none bring up the gui.
if __name__=="__main__":
    #set up the option parser
    usage = "usage: %prog [options]"
    parser = OptionParser()
    parser.add_option("-i", "--inFolder", dest = "inFolder",
                      help = "Folder to process. If left out, GUI will open.",
                      default = 'GUI')
    parser.add_option("-a", "--adj", dest = "adj",
                      help = """Adjustment, either (len, width) with parenthesis
                              or percent, default (640, 480)""",
                      default = '(640, 480)')
    parser.add_option("-t", "--adjType", dest = "adjType",
                      help = "pct, max, abs",
                      default = "max")
    parser.add_option("-o", "--outFolder", dest = "outFolder",
                      help = "out folder path",
                      default = "") #if left out, it will go to the inFolder
    parser.add_option("-p", "--prefix", dest = "prefix",
                      help = "Prefix to attach at beginning",
                      default = "")
    parser.add_option("-s", "--suffix", dest = "suffix",
                      help = "Suffix to attach at end",
                      default = "")
        
    (options, args) = parser.parse_args()    
    
    if options.inFolder == 'GUI' or len(options.inFolder) == 0:
            app = wx.PySimpleApp(0)
            wx.InitAllImageHandlers()
            MainGui = GuiFrame(None, -1, "")
            app.SetTopWindow(MainGui)
            MainGui.Show()
            app.MainLoop()
    else:
        #adj  should not be strings, so must be parsed
        try:
            if options.adj[0] + options.adj[-1] == '()':
                adj = tuple(map(float, options.adj[1:-1].split(',')))
            else:
                adj = float(options.adj)
    
        except Exception, inst:
            print 'Improper options'
            print inst        
    
        print 'adj = '
        print adj
        couldNotProcess = processFolder(options.inFolder, adj, options.adjType,
                                        options.outFolder, options.prefix, 
                                        options.suffix)
    
        if len(couldNotProcess) > 0:
            print "Completed, but could not process:"
            print ', \n'.join(couldNotProcess)
        else:
            print "Done"
    

